﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using MarioArena.World;
using MarioArena.Animation;
using Microsoft.Xna.Framework.Graphics;
using MarioArena.HUD;

namespace MarioArena.Characters
{
    public class Character
    {
        public float MaxSpeedX = 360;
        public float MaxSpeedY = 480; // Gravity
        public int MaxHealth = 100;
        public int Health = 100;
        public int Score = 0;
        public int Deaths = 0;

        /// <summary>
        /// Position on screen.
        /// </summary>
        public Vector2 Position { get; set; } // Called from GameManager.StartGame()
        public bool IsDead { get; set; }

        /// <summary>
        /// The camera position of the projectile origin.
        /// </summary>
        public Vector2 ProjectileOrigin { get; set; }
        public Rectangle Bounds { get; set; }
        public Point TileIndex = new Point(0, 0);
        public int Width = 24;
        public int Height = 39;
        public int HeightInTiles;
        public int WidthInTiles;
        public int MarginTop;
        public int MarginBottom;

        public float SpeedX { get; set; }
        public float SpeedY { get; set; }
        public float AccelerationX = 432f;
        public float DecelerationX = 936f;
        public float TurningDecelerationX = 1440f;
        public float AccelerationY = 936f; // Gravity
        public float AccelerationScale = 1.0f;
        public int Direction = 0;

        // Used for opponents to calculate the position on screen
        public int CameraX { get; set; } // 0 when used by player
        public int CameraY { get; set; } // 0 when used by player

        public PlayerState PlayerState = PlayerState.Passive;

        protected int mHalfScreenWidth;
        protected int mHalfScreenHeight;
        
        private bool mUsedByPlayer;
        private bool mFlashing = false;
        private long mTimeDamageStart;
        private AnimationSet mAnimationCollection;
        private HealthBar mHealthBar;

        private Tile mLeftTile, mRightTile, mTopTile, mBottomTile, mTopLeftTile, mTopRightTile, mBottomLeftTile, mBottomRightTile;
        private Texture2D dummyTexture;
        private Explosion mExplosion; // null if finished

        /// <summary>
        /// Constructs a new character. 
        /// </summary>
        /// <param name="animationCollection">A collection of animations used for this character. AnimationCollections are created by the GameManager and are public.</param>
        /// <param name="usedByPlayer">Indicates whether this character is being used by the player singleton or not.</param>
        protected Character(AnimationSet animationCollection, bool usedByPlayer)
        {
            mHalfScreenWidth = GameManager.GraphicsDevice.Viewport.Width / 2;
            mHalfScreenHeight = GameManager.GraphicsDevice.Viewport.Height / 2;
            this.PlayerState = PlayerState.Passive;

            this.MarginTop = this.Height - (Tile.Height / 2);
            this.MarginBottom = Tile.Height / 2;
            this.mAnimationCollection = animationCollection;
            this.mHealthBar = new HealthBar(this, usedByPlayer);
            this.mUsedByPlayer = usedByPlayer;
            this.Bounds = new Rectangle(Camera.X - (this.Width / 2), Camera.Y - this.MarginTop, this.Width, this.Height);

            float heightInTiles = Height / Tile.Height;
            if (heightInTiles == 0)
                heightInTiles = 1;
            if (Height % 24 != 0)
                heightInTiles = Convert.ToInt32(heightInTiles) + 1;
            this.HeightInTiles = Convert.ToInt32(heightInTiles);

            float widthInTiles = Width / Tile.Width;
            if (widthInTiles == 0)
                widthInTiles = 1;
            else if (Width % 24 != 0)
                widthInTiles = Convert.ToInt32(widthInTiles) + 1;
            this.WidthInTiles = Convert.ToInt32(widthInTiles);

            this.TileIndex = new Point((this.CameraX + mHalfScreenWidth) / Tile.Width, (this.CameraY + mHalfScreenHeight) / Tile.Height);

            dummyTexture = new Texture2D(GameManager.GraphicsDevice, 1, 1);
            dummyTexture.SetData(new Color[] { Color.White });
        }

        /// <summary>
        /// Updates the character.
        /// </summary>
        /// <param name="gameTime"></param>
        public void Update(GameTime gameTime)
        {
            if (!this.IsDead)
            {
                float elapsedTime = (float)gameTime.ElapsedGameTime.TotalSeconds;

                // Used the client's camera position's
                if (this.mUsedByPlayer)
                {
                    this.CameraX = Camera.X;
                    this.CameraY = Camera.Y;
                }
                // Otherwise the received camera positions are used

                this.mAnimationCollection.Update(gameTime, this.PlayerState);
                Collide(elapsedTime);

                // Keep track of the tile index used for optimal drawing and collision detection
                this.TileIndex = new Point((this.CameraX + mHalfScreenWidth) / Tile.Width, (this.CameraY + mHalfScreenHeight) / Tile.Height);

                // Flash the player when hit
                if (this.mFlashing)
                {
                    // Flash the player red when hit
                    long currentTime = DateTime.Now.Ticks / 10000;

                    if (currentTime - this.mTimeDamageStart <= Constants.DamageFlashDuration)
                    {
                        this.mAnimationCollection.AnimationColor = Color.Red;
                    }
                    else
                    {
                        this.mAnimationCollection.AnimationColor = Color.White;
                        this.mFlashing = false;
                    }
                }

                // Update the healthbar
                this.mHealthBar.Update();
            }
            else // Dead
            {
                if (this.mExplosion != null)
                {
                    if (!this.mExplosion.HasFinished)
                    {
                        this.mExplosion.Update(gameTime);
                    }
                }
            }
        }

        /// <summary>
        /// Draws the character.
        /// </summary>
        public void Draw(SpriteBatch spriteBatch)
        {
            if (!this.IsDead) // Only draw if not dead
            {
                if (this.Direction == -1)
                    this.mAnimationCollection.FlipHorizontally();
                else this.mAnimationCollection.UnFlipHorizontally();
                this.mAnimationCollection.FramesPerSecond = Convert.ToInt32((this.SpeedX / 60) * 3); // / 60 for 60 fps

                if (this.mUsedByPlayer == true) // If this character instance is the player, draw the player in the middle of the screen
                {
                    this.mAnimationCollection.Draw(spriteBatch, this.Position - new Vector2(0, (this.Height / 2)), this.PlayerState);
                }

                else // Draw the opponent at the correct position
                {
                    Vector2 opponentPosition = new Vector2(
                        this.Position.X - (Camera.X - this.CameraX) - (this.Width / 2) + GameManager.Instance.HalfScreenWidth,
                        this.Position.Y - (Camera.Y - this.CameraY) - (this.Height / 2) - this.MarginBottom + GameManager.Instance.HalfScreenHeight
                        );
                    this.mAnimationCollection.Draw(spriteBatch, opponentPosition, this.PlayerState);
                }

                // Draw the healthbar
                this.mHealthBar.Draw(spriteBatch);
            }
            else // Dead, draw explosion
            {
                if (this.mExplosion != null)
                {
                    if (!this.mExplosion.HasFinished)
                    {
                        this.mExplosion.Draw(spriteBatch);
                    }
                }
            }

            // player bounds
            //spriteBatch.Draw(dummyTexture, new Rectangle(this.Bounds.X - Camera.X + mHalfScreenWidth, this.Bounds.Y - Camera.Y + mHalfScreenHeight, this.Bounds.Width, this.Bounds.Height), Color.Black);

            // projectileorigin
            //spriteBatch.Draw(dummyTexture, new Rectangle((int)this.ProjectileOrigin.X + mHalfScreenWidth - Camera.X, (int)this.ProjectileOrigin.Y + mHalfScreenHeight - Camera.Y, 5, 5), Color.Yellow);
            
            // player position
            //spriteBatch.Draw(dummyTexture, new Rectangle(this.CameraX + mHalfScreenWidth - Camera.X, this.CameraY + mHalfScreenHeight - Camera.Y, 5, 5), Color.Yellow);
        }

        /// <summary>
        /// Checks whether the tiles next to the character (if present) are within the bounds 
        /// of the tilemap. This method is used to prevent index-out-of-range-exceptions.
        /// </summary>
        /// <returns>Whether the character's current tileindex is within the 
        /// bounds of the tilemap.</returns>
        public bool IsTileIndexInBounds()
        {
            if (TileIndex.X > 0 && TileIndex.Y > HeightInTiles - 1 &&
                TileIndex.X < TileEngine.Instance.LevelWidth - 1
                && TileIndex.Y < TileEngine.Instance.LevelHeight - 1)
            {
                return true;
            }
            return false;
        }

        /// <summary>
        /// Damages the player, flashing the player red shortly.
        /// </summary>
        /// <param name="damage">Amount of damage the player takes.</param>
        public void Damage(int damage)
        {
            // TODO: Damage the player depending on projectile
            this.Health -= damage;
            

            this.mTimeDamageStart = DateTime.Now.Ticks / 10000;
            this.mFlashing = true;
        }

        /// <summary>
        /// Makes this player die.
        /// </summary>
        public void Die()
        {
            this.Health = 0;
            this.IsDead = true;
            this.Deaths++; //add a death
            GameManager.Instance.DeathScreamSound.Play(GameManager.Instance.GameVolume * 0.6f, 0, 0);
            GameManager.Instance.DeathExplosionSound.Play(GameManager.Instance.GameVolume, 0, 0);

            int explosionMarginBottom = 10;
            Vector2 explosionPosition = new Vector2(Camera.X, Camera.Y - explosionMarginBottom) - this.Position + new Vector2(GameManager.Instance.HalfScreenWidth, GameManager.Instance.HalfScreenHeight); // Player position
            if (this.mUsedByPlayer == false) // This character is an opponent
                explosionPosition = new Vector2(CameraX, CameraY - explosionMarginBottom);
            this.mExplosion = new Explosion(explosionPosition, Explosion.Type.Death);

            // Set the position to prevent UDP packages to be sent which suggest that the player is 
            // still in a hole, which would cause the character to die instantly after respawning.
            if (mUsedByPlayer)
            {
                if (Camera.Y > TileEngine.Instance.LevelHeight * Tile.Width)
                    Camera.Y = (TileEngine.Instance.LevelHeight * Tile.Width) - Tile.Width - Height;
            }

            // Reset speed
            this.SpeedX = 0;
            this.SpeedY = 0;
            this.PlayerState = PlayerState.Passive;
        }

        /// <summary>
        /// Awards this player with a point.
        /// </summary>
        public void AwardPoint()
        {
            this.Score++;
        }

        //Respawns this player.
        public void Respawn(int tileIndexX, int tileIndexY)
        {
            this.Health = 100;
            this.IsDead = false;

            if (mUsedByPlayer)//player uses this character
            {
                Camera.X = 0;
                Camera.Y = 0;
                int spawnPosX = (GameManager.GraphicsDevice.Viewport.Width / 2) - (Tile.Width / 2);
                int spawnPosY = (GameManager.GraphicsDevice.Viewport.Height / 2) - (Tile.Height / 2);
                Camera.X = (Camera.X - spawnPosX) + (tileIndexX * 24);
                Camera.Y = (Camera.Y - spawnPosY) + (tileIndexY * 24);
            }
        }

        /// <summary>
        /// Collides with tiles if applicable, and updates the camera position when this character is used in the Player class.
        /// </summary>
        private void Collide(float elapsedTime)
        {
            this.Bounds = new Rectangle(this.CameraX - (this.Width / 2), this.CameraY - this.MarginTop, this.Width, this.Height);
            Point nextCameraPosition = new Point(this.CameraX + Convert.ToInt32((this.SpeedX * this.Direction) * elapsedTime), this.CameraY + Convert.ToInt32((this.SpeedY) * elapsedTime));
            Rectangle nextBounds = new Rectangle(nextCameraPosition.X - (this.Width / 2), nextCameraPosition.Y - this.MarginTop, this.Width, this.Height);

            Tile[,] tiles = TileEngine.Instance.Tiles;
            int leftIndex = this.TileIndex.X - 1;
            int rightIndex = this.TileIndex.X + 1;
            int topIndex = this.TileIndex.Y - (Convert.ToInt32(this.HeightInTiles));
            int bottomIndex = this.TileIndex.Y + 1;

            // Check if the tileindex isn't out of bounds
            if (IsTileIndexInBounds())
            {
                mLeftTile = tiles[leftIndex, this.TileIndex.Y];
                mLeftTile.Bounds = new Rectangle(mLeftTile.Bounds.X - this.mHalfScreenWidth, mLeftTile.Bounds.Y - this.mHalfScreenHeight, mLeftTile.Bounds.Width, mLeftTile.Bounds.Height);
                mRightTile = tiles[rightIndex, this.TileIndex.Y];
                mRightTile.Bounds = new Rectangle(mRightTile.Bounds.X - this.mHalfScreenWidth, mRightTile.Bounds.Y - this.mHalfScreenHeight, mRightTile.Bounds.Width, mRightTile.Bounds.Height);
                mTopTile = tiles[this.TileIndex.X, topIndex];
                mTopTile.Bounds = new Rectangle(mTopTile.Bounds.X - this.mHalfScreenWidth, mTopTile.Bounds.Y - this.mHalfScreenHeight, mTopTile.Bounds.Width, mTopTile.Bounds.Height);
                mBottomTile = tiles[this.TileIndex.X, bottomIndex];
                mBottomTile.Bounds = new Rectangle(mBottomTile.Bounds.X - this.mHalfScreenWidth, mBottomTile.Bounds.Y - this.mHalfScreenHeight, mBottomTile.Bounds.Width, mBottomTile.Bounds.Height);

                mTopLeftTile = tiles[this.TileIndex.X - 1, topIndex];
                mTopLeftTile.Bounds = new Rectangle(mTopLeftTile.Bounds.X - this.mHalfScreenWidth, mTopLeftTile.Bounds.Y - this.mHalfScreenHeight, mTopLeftTile.Bounds.Width, mTopLeftTile.Bounds.Height);
                mTopRightTile = tiles[this.TileIndex.X + 1, topIndex];
                mTopRightTile.Bounds = new Rectangle(mTopRightTile.Bounds.X - this.mHalfScreenWidth, mTopRightTile.Bounds.Y - this.mHalfScreenHeight, mTopRightTile.Bounds.Width, mTopRightTile.Bounds.Height);
                mBottomLeftTile = tiles[this.TileIndex.X - 1, bottomIndex];
                mBottomLeftTile.Bounds = new Rectangle(mBottomLeftTile.Bounds.X - this.mHalfScreenWidth, mBottomLeftTile.Bounds.Y - this.mHalfScreenHeight, mBottomLeftTile.Bounds.Width, mBottomLeftTile.Bounds.Height);
                mBottomRightTile = tiles[this.TileIndex.X + 1, bottomIndex];
                mBottomRightTile.Bounds = new Rectangle(mBottomRightTile.Bounds.X - this.mHalfScreenWidth, mBottomRightTile.Bounds.Y - this.mHalfScreenHeight, mBottomRightTile.Bounds.Width, mBottomRightTile.Bounds.Height);
            }

            #region LEFT tile collision
            if (this.Direction == -1)
            {
                // Player can collide with multiple tiles to his left
                for (int i = 0; i < HeightInTiles; i++)
                {
                    // Check if index is not out of range
                    if (TileIndex.X - 1 >= 0 && TileIndex.X - 1 < TileEngine.Instance.LevelWidth 
                        && this.TileIndex.Y - i >= 0 && this.TileIndex.Y - i < tiles.GetLength(1))
                    {
                        Tile leftTile = tiles[this.TileIndex.X - 1, this.TileIndex.Y - i];
                        if (leftTile.Texture != null)
                        {
                            leftTile.Bounds = new Rectangle(leftTile.Bounds.X - this.mHalfScreenWidth, leftTile.Bounds.Y - this.mHalfScreenHeight, leftTile.Bounds.Width, leftTile.Bounds.Height);
                            if (nextBounds.Intersects(leftTile.Bounds))
                            {
                                // Don't check the wrong X position when checking vertical collision, because the X position has already been corrected here
                                nextBounds = new Rectangle(leftTile.Bounds.Right, this.Bounds.Y, this.Bounds.Width, this.Bounds.Height);

                                // Stop the player
                                this.SpeedX = 0;

                                if (this.mUsedByPlayer)
                                    Camera.X = nextBounds.X + (this.Width / 2);
                                this.CameraX = nextBounds.X + (this.Width / 2);

                                i = this.HeightInTiles; // Collision detected, no need to check any further
                            }
                        }
                    }
                }
            }
            #endregion
            #region RIGHT tile collision
            if (this.Direction == 1)
            {
                // Player can collide with multiple tiles to his right
                for (int i = 0; i < HeightInTiles; i++)
                {
                    // If the index is not out of range
                    if (this.TileIndex.X + 1 < TileEngine.Instance.LevelWidth && this.TileIndex.X + 1 >= 0 
                        && this.TileIndex.Y - i >= 0 && this.TileIndex.Y < tiles.GetLength(1))
                    {
                        Tile rightTile = tiles[this.TileIndex.X + 1, this.TileIndex.Y - i];
                        if (rightTile.Texture != null)
                        {
                            rightTile.Bounds = new Rectangle(rightTile.Bounds.X - this.mHalfScreenWidth, rightTile.Bounds.Y - this.mHalfScreenHeight, rightTile.Bounds.Width, rightTile.Bounds.Height);
                            if (nextBounds.Intersects(rightTile.Bounds))
                            {
                                // Don't check the wrong X position when checking vertical collision, because the X position has already been corrected here
                                nextBounds = new Rectangle(rightTile.Bounds.Left - Tile.Width, this.Bounds.Y, this.Bounds.Width, this.Bounds.Height);

                                // Stop the player
                                this.SpeedX = 0;

                                if (this.mUsedByPlayer)
                                    Camera.X = nextBounds.X + (this.Width / 2);
                                this.CameraX = nextBounds.X + (this.Width / 2);

                                i = this.HeightInTiles; // Collision detected, no need to check any further
                            }
                        }
                    }
                }
            }
            #endregion
            #region TOP tile collision
            if (mTopTile.Texture != null || mTopLeftTile.Texture != null || mTopRightTile.Texture != null)
            {
                if (nextBounds.Intersects(mTopTile.Bounds) || nextBounds.Intersects(mTopLeftTile.Bounds) || nextBounds.Intersects(mTopRightTile.Bounds))
                {
                    if (nextBounds.Intersects(mTopTile.Bounds))
                        nextBounds = new Rectangle(this.Bounds.X, mTopTile.Bounds.Y + Tile.Height, this.Bounds.Width, this.Bounds.Height);
                    else if (nextBounds.Intersects(mTopLeftTile.Bounds))
                        nextBounds = new Rectangle(this.Bounds.X, mTopLeftTile.Bounds.Y + Tile.Height, this.Bounds.Width, this.Bounds.Height);
                    else if (nextBounds.Intersects(mTopRightTile.Bounds))
                        nextBounds = new Rectangle(this.Bounds.X, mTopRightTile.Bounds.Y + Tile.Height, this.Bounds.Width, this.Bounds.Height);

                    this.SpeedY = 0;

                    if (this.mUsedByPlayer) // Only update camera if this instance is used in the Player singleton
                        Camera.Y = nextBounds.Y + this.MarginTop;
                    this.CameraY = nextBounds.Y + this.MarginTop;
                }
            }
            #endregion
            #region BOTTOM tile collision
            // BOTTOM LEFT, BOTTOM MID, BOTTOM RIGHT
            if (mBottomTile.Texture != null || mBottomLeftTile.Texture != null || mBottomRightTile.Texture != null)
            {
                if ((nextBounds.Intersects(mBottomTile.Bounds) || nextBounds.Intersects(mBottomLeftTile.Bounds) || nextBounds.Intersects(mBottomRightTile.Bounds))
                    && PlayerState != PlayerState.Jumping)
                {
                    if (nextBounds.Intersects(mBottomTile.Bounds))
                        nextBounds = new Rectangle(this.Bounds.X, mBottomTile.Bounds.Y - Tile.Height, this.Bounds.Width, this.Bounds.Height);
                    else if (nextBounds.Intersects(mBottomLeftTile.Bounds))
                        nextBounds = new Rectangle(this.Bounds.X, mBottomLeftTile.Bounds.Y - Tile.Height, this.Bounds.Width, this.Bounds.Height);
                    else if (nextBounds.Intersects(mBottomRightTile.Bounds))
                        nextBounds = new Rectangle(this.Bounds.X, mBottomRightTile.Bounds.Y - Tile.Height, this.Bounds.Width, this.Bounds.Height);

                    this.SpeedY = 0;
                    if (this.SpeedX <= 0) // Player isn't falling anymore after collision, so set the correct new playerstate
                        this.PlayerState = PlayerState.Passive;
                    else this.PlayerState = PlayerState.Running;

                    if (this.mUsedByPlayer) // Only update camera if this instance is used in the Player singleton
                        Camera.Y = nextBounds.Y + this.MarginBottom;
                    this.CameraY = nextBounds.Y + this.MarginBottom;
                }
            }
            #endregion

            // Position the (fake/ghost) next bounds in the bottom tiles, to check whether any tiles are being stood on
            nextBounds = new Rectangle(nextBounds.X, nextBounds.Y + 5, nextBounds.Width, nextBounds.Height);

            // Check if the player should start falling
            if (PlayerState == PlayerState.Passive || PlayerState == PlayerState.Running)
            {
                if (!(nextBounds.Intersects(mBottomLeftTile.Bounds) || nextBounds.Intersects(mBottomRightTile.Bounds) || nextBounds.Intersects(mBottomTile.Bounds)))
                {
                    this.PlayerState = PlayerState.Falling;
                }
                else if (PlayerState != PlayerState.Falling && PlayerState != PlayerState.Jumping)
                {
                    // Check if the player should be either passive or running
                    if (this.SpeedX <= 0)
                        this.PlayerState = PlayerState.Passive;
                    else this.PlayerState = PlayerState.Running;
                }
            }

        }
    }
}
